﻿using System;
using System.Linq;
using System.Text.RegularExpressions;
using System.Xml;

namespace Kernelx.Extensions
{
    /// <summary>
    /// 
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="node"></param>
    /// <param name="value"></param>
    public delegate void XmlWrite<T>(XmlNode node, T value);
    /// <summary>
    /// 
    /// </summary>
    public static class XmlDocumentExtensions
    {

        #region 定义常量


        const string DefaultRootNodeName = "Kernelx.Configs";

#if DEBUG
        /// <summary>
        /// 初步检查xpath是否非法
        /// </summary>  
#endif
        const string Xpath_RegexIllegal = @"(/{3,}|\.{2,})";
#if DEBUG
        /// <summary>
        /// 匹配xpath中的一项节点信息(目前只支持指定一项明确的属性检索信息)
        /// </summary>  
#endif
        const string Xpath_RegexNodeInfo = @"^[a-zA-Z][a-zA-Z0-9]*([\._-][a-zA-Z0-9]+)*(\[@[a-zA-Z][a-zA-Z0-9]*='[a-zA-Z0-9\u4e00-\u9fa5][a-zA-Z0-9\u4e00-\u9fa5\._]*'\])?$";
#if DEBUG
        /// <summary>
        /// 配置一项节点信息中的节点名
        /// </summary>  
#endif
        const string Xpath_RegexNodeName = @"^[a-zA-Z][a-zA-Z0-9]*([\._-][a-zA-Z0-9]+)*";
#if DEBUG
        /// <summary>
        /// 匹配一项节点信息中的属性信息
        /// </summary>  
#endif
        const string Xpath_RegexPropKeyValue = @"@([a-zA-Z][a-zA-Z0-9]*)='([a-zA-Z0-9\u4e00-\u9fa5][a-zA-Z0-9\u4e00-\u9fa5\._]*)'";

        //#if DEBUG
        //        /// <summary>
        //        /// 定义非打印低位字符正则式
        //        /// </summary>  
        //#endif
        //        const string LowCharsCheck = @"[\x00-\x08]|[\x0B-\x0C]|[\x0E-\x1F]|(&#x0?[0-8B-C];)|(&#x0?[E-F];)|(&#x1[0-F];)";

        #endregion

#if DEBUG
        /// <summary>
        /// 反向递归解析Xpath各节点内容
        /// </summary>  
#endif
        class NodeXpath
        {

            internal string CurrentXpath { get; private set; }
            internal string NodeName { get; private set; }
            internal string KeyPropName { get; private set; }
            internal string KeyPropValue { get; private set; }
            //internal Dictionary<string, string> Attributes {get; set;}    //存储该节点的属性信息(暂不实现)


            public NodeXpath ParentXpath { get; private set; }

#if DEBUG
            /// <summary>
            /// 限定的xpath必须提供叶节点(必须是明确精准定位的且每层节点名支持一项等值的属性检索信息);
            /// <para>xpath表达式中的一项节点信息必须满足^[a-zA-Z][a-zA-Z0-9]*([\._-][a-zA-Z0-9]+)*(\[@[a-zA-Z][a-zA-Z0-9]*='[a-zA-Z0-9][a-zA-Z0-9\._]+'\])?$</para>
            /// </summary>
            /// <param name="limitXpath"></param>  
#endif
            internal NodeXpath(string limitXpath)
            {
                if (string.IsNullOrEmpty(limitXpath) || limitXpath.StartsWith("/") && !limitXpath.StartsWith("//"))
                    throw new ApplicationException($"{nameof(limitXpath)}不能为空串,且不能以单个/开头");

                var illegalMatch = Regex.Match(limitXpath, Xpath_RegexIllegal);
                if (!illegalMatch.Success)
                {
                    CurrentXpath = limitXpath;
                    int index = CurrentXpath.LastIndexOf('/');
                    var nodeinfo = Regex.Match(index >= 0 ? CurrentXpath.Substring(index + 1) : CurrentXpath, Xpath_RegexNodeInfo);
                    if (nodeinfo.Success)
                    {
                        NodeName = Regex.Match(nodeinfo.Value, Xpath_RegexNodeName).ToString();
                        var prop = Regex.Match(nodeinfo.Value, Xpath_RegexPropKeyValue);
                        if (prop.Success)
                        {
                            KeyPropName = prop.Groups[1].Value;
                            KeyPropValue = prop.Groups[2].Value;
                        }
                    }
                    else
                        throw new ArgumentException($"参数{nameof(limitXpath)}不是合法的限定Xpath表达式:{nameof(limitXpath)}中的一项节点信息(目前最多只支持指定一项明确的属性检索信息)");
                    if (index > 2 || index == 1 && CurrentXpath[0] != '/')
                    {
                        if (CurrentXpath[index - 1] == '/')
                            --index;
                        ParentXpath = new NodeXpath(CurrentXpath.Substring(0, index));
                    }
                    else if (index == 0 || index == 1 && CurrentXpath[0] == '/')
                        ParentXpath = new NodeXpath();
                }
                else
                    throw new ArgumentException($"参数{nameof(limitXpath)}不是合法的Xpath表达式");
            }

            private NodeXpath() { }


        }

        private static void DefaultXmlWrite<T>(XmlNode node, T value)
        {
            var type = typeof(T);
            if (type == typeof(string))
            {
                node.InnerText = value as string;
            }
            else if (type.IsValueType)
            {
                node.InnerText = value.ToString();
            }
            else
            {
                if (value == null) return;

                var props = type.GetProperties();
                var ns = node.Attributes.Cast<XmlAttribute>().Select(n => n.Name);
                var cns = node.ChildNodes.Cast<XmlNode>();
                foreach (var p in props)
                {
                    var v = p.GetValue(value, null);
                    if (!ns.Contains(p.Name))
                    {
                        XmlNode cn = null;
                        if ((cn = cns.FirstOrDefault(x => x.Name == p.Name)) == null)
                        {
                            cn = node.OwnerDocument.CreateElement(p.Name);
                            cn.InnerText = v.ToString();
                            node.AppendChild(cn);
                        }
                        else
                            cn.InnerText = v.ToString();
                    }
                    else
                        node.Attributes[p.Name].Value = v.ToString();
                }
            }
        }

        /// <summary>
        /// 将@value对象写入到指定xpath路径下的doc中
        /// </summary>
        /// <typeparam name="T"></typeparam>
        /// <param name="doc"></param>
        /// <param name="limitXpath">限定的xpath表达式(必须是明确精准定位的且每层节点名支持一项等值的属性检索信息)</param>
        /// <param name="value"></param>
        /// <param name="write"></param>
        /// <returns></returns>
        public static XmlDocument WriteTo<T>(this XmlDocument doc, string limitXpath, T @value, XmlWrite<T> write = null)
        {
            if (doc == null)
                throw new ApplicationException($"必须提供{nameof(doc)}文档对象");
            if (write == null)
                write = DefaultXmlWrite;
            var xpath = new NodeXpath(limitXpath);

            lock (doc)
            {
                var node = doc.SelectSingleNode(xpath.CurrentXpath);
                if (node == null)
                {
                    node = doc.CreateElement(xpath.NodeName);
                    if (!string.IsNullOrEmpty(xpath.KeyPropName))
                    {
                        var att = doc.CreateAttribute(xpath.KeyPropName);
                        att.Value = xpath.KeyPropValue;
                        node.Attributes.Append(att);
                    }

                    var newNode = node;
                    var rootNode = doc.FirstChild ?? doc;
                    var pXpath = xpath.ParentXpath;
                    if (pXpath != null && !string.IsNullOrEmpty(pXpath.NodeName))
                    {
                        XmlNode pNode = doc.SelectSingleNode(pXpath.CurrentXpath);
                        while (pNode == null)
                        {
                            newNode = doc.CreateElement(pXpath.NodeName).AppendChild(newNode).ParentNode;
                            if (!string.IsNullOrEmpty(pXpath.KeyPropName))
                            {
                                var att = doc.CreateAttribute(pXpath.KeyPropName);
                                att.Value = pXpath.KeyPropValue;
                                newNode.Attributes.Append(att);
                            }
                            pXpath = pXpath.ParentXpath;
                            if (pXpath == null)
                                pNode = doc.FirstChild ?? doc;
                            else
                            {
                                if (string.IsNullOrEmpty(pXpath.NodeName) && doc.FirstChild == null)
                                    pXpath = new NodeXpath(DefaultRootNodeName);

                                pNode = string.IsNullOrEmpty(pXpath.NodeName) ? doc.FirstChild : doc.SelectSingleNode(pXpath.CurrentXpath);
                            }
                        }
                        rootNode = pNode;
                    }
                    rootNode.AppendChild(newNode);
                }
                write(node, @value);
            }

            return doc;
        }


    }
}
